﻿/* HEADER
 * ------
 * © 2009 by Salomon Zwecker 
 * modified by:
 * - 
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Shapes.Misc;

namespace Shapes.Geometry
{
    /// <summary>
    /// A Shape created from a texture
    /// </summary>
    public sealed class SpriteShape : Shape
    {

        bool _IsBorderComputed, _IsBorderSorted;

        Texture2D _Texture;
        /// <summary>
        /// The texture which is used to create the shape
        /// </summary>
        public Texture2D Texture { get { return _Texture; } }

        Rectangle? _SourceRect;
        /// <summary>
        /// The SourceRectangle of the Texture
        /// </summary>
        public Rectangle? SourceRect
        {
            get { _IsBorderComputed = false; return _SourceRect; }
            set {
                _IsBorderComputed = _IsBorderSorted = false;
                _SourceRect = value;
                CalculateDimension();
            }
        }

        string _TextureAssetName;
        /// <summary>
        /// The asset name of the texture. Set it to change the texture.
        /// </summary>
        public string TextureAssetName
        {
            get { return _TextureAssetName; }
            set {
                _IsBorderComputed = _IsBorderSorted = false;
                _TextureAssetName = value;
                KeyValuePair<Texture2D, Color[]> pair = ShapeTexturePool.Load(value);
                _Texture = pair.Key;
                _Pixels = pair.Value;
                CalculateDimension();
            }
        }

        Color[] _Pixels;
        internal List<Vector2> _Border = new List<Vector2>();
        
        internal IColorCondition _Condition;

        
        /// <summary>
        /// Creates a new SpriteShape
        /// </summary>
        /// <param name="textureAssetName">the name of the Texture</param>
        /// <param name="condition">pass a 'AlphaThreshold' or 'ColorMap' object here</param>
        /// <remarks>The asset name is used instead of the texture itself to be able to easily save and load the shape from xml data</remarks>
        public SpriteShape(string textureAssetName, IColorCondition condition)
            : this(textureAssetName, null, condition)
        {
        }
        /// <summary>
        /// Creates a new SpriteShape
        /// </summary>
        /// <param name="textureAssetName">the name of the Texture</param>
        /// <param name="sourceRect">a rectangle which section of the texture is used for the shape (pass 'null' for the entire texture)</param>
        /// <param name="condition">pass a 'AlphaThreshold' or 'ColorMap' object here</param>
        /// <remarks>The asset name is used instead of the texture itself to be able to easily save and load the shape from xml data</remarks>
        public SpriteShape(string textureAssetName, Rectangle? sourceRect, IColorCondition condition)
            : base()
        {
            if (!ShapeTexturePool.IsInitialized)
                throw new ShapeTexturePoolNotInitializedException();

            _TextureAssetName = textureAssetName;
            KeyValuePair<Texture2D, Color[]> pair = ShapeTexturePool.Load(textureAssetName);
            _Texture = pair.Key;
            _Pixels = pair.Value;

            _Condition = condition;

            _SourceRect = sourceRect;
            CalculateDimension();
        }



        internal override bool IsPointInside(ref Vector2 point)
        {
            Vector2 p;
            p.X = point.X;// -_Bounding._Position.X;
            p.Y = point.Y;// -_Bounding._Position.Y;

            float minX = 0;
            if (_SourceRect != null)
            {
                Rectangle rect = (Rectangle)_SourceRect;

                p.X += rect.X;
                p.Y += rect.Y;
                minX = rect.X;
            }

            if (p.X < minX || p.X >= _Transform._Width + minX)
                return false;

            int idx = (int)p.X + (int)p.Y * _Texture.Width;


            return ((idx >= 0) && (idx < _Pixels.Length))
                && _Condition.CheckColorCondition(ref _Pixels[idx]);

            //if ((idx >= 0) && (idx < _Pixels.Length)    // in range?
            //    && (_Pixels[idx].A >= _Alpha))          // not transparent?
            //    return true;

            //return false;
        }
        /// <summary>
        /// a test to check for the distance of a point to the nearest edge
        /// </summary>
        /// <param name="point">the point to check</param>
        /// <returns>the distance to the nearest edge</returns>
        internal override float GetDistanceToEdge(ref Vector2 point)
        {
            return Vector2.Distance(GetNearestPointOnEdge(ref point), point);
        }

        internal override Vector2 GetNearestPointOnEdge(ref Vector2 point)
        {
            return GetNearestPointOnEdge(ref point, _Border);
        }
        private Vector2 GetNearestPointOnEdge(ref Vector2 point, List<Vector2> source)
        {
            ComputeBorder();

            //Vector2 p;
            //p.X = point.X;// - _Bounding._Position.X;
            //p.Y = point.Y;// - _Bounding._Position.Y;

            if (source.Count == 0)
                return _Transform.Position;

            Vector2 nearest = source[0];

            foreach (Vector2 v in source)
            {
                if (Vector2.DistanceSquared(v, point) < Vector2.DistanceSquared(nearest, point))
                {
                    nearest = v;
                }
            }
            return nearest;
        }

        private void CalculateDimension()
        {
            if (_SourceRect == null)
            {
                _Transform._Width = _Texture.Width;
                _Transform._Height = _Texture.Height;
            }
            else
            {
                Rectangle rect = ((Rectangle)_SourceRect);
                _Transform._Width = rect.Width;
                _Transform._Height = rect.Height;
            }
            CallOnChange();
        }

        /// <summary>
        /// Clones the SpriteShape.
        /// </summary>
        /// <returns>an object with the type SpriteShape</returns>
        public override object Clone()
        {
            SpriteShape s = new SpriteShape(_TextureAssetName, _SourceRect, _Condition)
                                {
                                    _Transform = (Transformation2D) _Transform.Clone()
                                };
            return s;
        }

        void ComputeBorder()
        {
            if (_IsBorderComputed)
                return;

            Rectangle rect;
            if (_SourceRect == null)
                rect = new Rectangle(0, 0, _Texture.Width, _Texture.Height);
            else
                rect = (Rectangle)_SourceRect;

            bool isInside = false;
            int idx;
            Vector2 coordinate = new Vector2();
            // horizontal
            for (int i = 0; i < rect.Width * rect.Height; i++)
            {
                //break;
                coordinate = new Vector2(i % rect.Width, (i - (i % rect.Width)) / rect.Width);

                idx = (rect.X + (int)coordinate.X) + (rect.Y + (int)coordinate.Y) * _Texture.Width;
                
                //System.Diagnostics.Trace.WriteLine("i: " + i +
                //  "\tCoordinate: X: " + coordinate.X + "\t\t Y: " + coordinate.Y
                //  + "\t\t\t idx: " + idx);

                if (((!isInside) && (_Condition.CheckColorCondition(ref _Pixels[idx])))
                    || ((isInside) && (!_Condition.CheckColorCondition(ref _Pixels[idx]))))
                {
                    _Border.Add(coordinate);
                    isInside = !isInside;
                }
                if ((coordinate.X + 1) % rect.Width == 0) // on the right
                {
                    if (isInside)
                    {
                        _Border.Add(coordinate);
                    }
                    isInside = false;
                }
            }
            isInside = false;
            // vertical
            for (int i = 0; i < rect.Width * rect.Height; i++)
            {
                coordinate.X =
                     (i * _Texture.Width - ((i * _Texture.Width) % (rect.Height * _Texture.Width)))
                    / (rect.Height * _Texture.Width);

                coordinate.Y =
                    (i * _Texture.Width) % (rect.Height * _Texture.Width) / _Texture.Width;


                if (_Border.Contains(coordinate))
                    continue;

                /*
                idx = rect.X + rect.Y * _Texture.Width                                              // initial offset
                    + (i * _Texture.Width) % (rect.Height * _Texture.Width)                         // y-position
                    + (i * _Texture.Width - (i * _Texture.Width) % (rect.Height * _Texture.Width))  // x-position
                    / (_Texture.Width * rect.Height);                               
                */

                idx = (rect.X + (int)coordinate.X) + (rect.Y + (int)coordinate.Y) * _Texture.Width;

                //System.Diagnostics.Trace.WriteLine("i: " + i + 
                //    "\tCoordinate: X: " + coordinate.X + "\t\t Y: " + coordinate.Y
                //    + "\t\t\t idx: " + idx);


                if (((!isInside) && (_Condition.CheckColorCondition(ref _Pixels[idx])))
                    || ((isInside) && (!_Condition.CheckColorCondition(ref _Pixels[idx]))))
                {
                    _Border.Add(coordinate);
                    isInside = !isInside;
                }
                if ((coordinate.Y + 1) % rect.Height == 0) // on the bottom
                {
                    if (isInside)
                    {
                        _Border.Add(coordinate);
                    }
                    isInside = false;
                }
            }
            _BorderLength = _Border.Count;

            _IsBorderComputed = true;
        }

        void SortBorder()
        {
            if (_IsBorderSorted)
                return;

            ComputeBorder();

            List<Vector2> tmp = new List<Vector2>();
            Vector2 near = Vector2.Zero;
            Vector2 next;

            while(_Border.Count > 0)
            {
                next = GetNearestPointOnEdge(ref near);

                // ** ? **
               // if (Vector2.DistanceSquared(GetNearestPointOnEdge(ref next, tmp), next) > Vector2.DistanceSquared(near, next))
                {
                     tmp.Add(next);
                }
                _Border.Remove(next);
                near = next;
            }

            _Border = tmp.ToList<Vector2>();
            //_BorderLength = _Border.Count;

            _IsBorderSorted = true;
        }

        internal override Vector2 GetPositionFromT(float t)
        {
            SortBorder();

            int idx = Math.Min((int)Math.Round(t * _BorderLength), _Border.Count - 1);

            return _Border[idx];
        }

        internal override float GetEdgePathValueFromPoint(ref Vector2 point)
        {

            return (float)GetBorderIndex(ref point) / _Border.Count;
        }

        private int GetBorderIndex(ref Vector2 point)
        {
            SortBorder();

            int idx = _Border.IndexOf(point);

            if (idx == -1)
                idx = _Border.IndexOf(GetNearestPointOnEdge(ref point));

            return idx;
        }

        private Vector2 GetTangent(int startIndex, params int[] steps)
        {

            float x = 0;
            float y = 0;
            
            for (int i = 0; i < steps.Length; i++)
            {
                float scale = 1 / steps[i];

                x += scale * (_Border[(startIndex + steps[i]) % _Border.Count].X - _Border[startIndex].X);
                y += scale * (_Border[(startIndex + steps[i]) % _Border.Count].Y - _Border[startIndex].Y);

                x += scale * (_Border[startIndex].X - _Border[(startIndex - steps[i] + _Border.Count) % _Border.Count].X);
                y += scale * (_Border[startIndex].Y - _Border[(startIndex - steps[i] + _Border.Count) % _Border.Count].Y);
            }

            return Vector2.Normalize(new Vector2(x, y));
        }
        /// <summary>
        /// gets a direction Vector which touches the given position witout crossing the outline at that position
        /// </summary>
        /// <param name="position">the position on the edge. should be part of the outline.</param>
        /// <param name="steps">the distances from the position along the outline which is used to compute the tangent. by default it is {1, 3}</param>
        /// <returns>tangent vector</returns>
        public Vector2 GetTangent(Vector2 position, params int[] steps)
        {
            _Transform.TransformGlobalToLocal(ref position);
            return GetTangent(GetBorderIndex(ref position), steps);
        }
        /// <summary>
        /// gets a direction Vector which touches the given position witout crossing the outline at that position
        /// </summary>
        /// <param name="position">the position on the edge. should be part of the outline.</param>
        /// <param name="isLocalCoordinate">if true, the given point is handled in local space (will not be transformed). 
        /// otherwise it is handled as parent space (global space, if it isn't attatched to another transform).</param>
        /// <param name="steps">the distances from the position along the outline which is used to compute the tangent. by default it is {1, 3}</param>
        /// <returns>tangent vector</returns>
        public Vector2 GetTangent(Vector2 position, bool isLocalCoordinate, params int[] steps)
        {
            if (!isLocalCoordinate)
                _Transform.TransformGlobalToLocal(ref position);

            return GetTangent(GetBorderIndex(ref position), steps);
        }

        internal override Vector2 GetTangent(ref Vector2 position)
        {
            return GetTangent(GetBorderIndex(ref position), 1, 3);
        }

        /// <summary>
        /// gets a direction Vector which crosses the outline at the given position with 90°
        /// </summary>
        /// <param name="position">the position on the edge. should be part of the outline.</param>
        /// <param name="steps">the distances from the position along the outline which is used to compute the normal. by default it is {1, 3}</param>
        /// <returns>normal vector</returns>
        public Vector2 GetNormal(Vector2 position, params int[] steps)
        {
            _Transform.TransformGlobalToLocal(ref position);
            Vector2 tangent = GetTangent(GetBorderIndex(ref position), steps);
            return new Vector2(-tangent.Y, tangent.X);
        }
        /// <summary>
        /// gets a direction Vector which crosses the outline at the given position with 90°
        /// </summary>
        /// <param name="position">the position on the edge. should be part of the outline.</param>
        /// <param name="isLocalCoordinate">if true, the given point is handled in local space (will not be transformed). 
        /// otherwise it is handled as parent space (global space, if it isn't attatched to another transform).</param>
        /// <param name="steps">the distances from the position along the outline which is used to compute the normal. by default it is {1, 3}</param>
        /// <returns>normal vector</returns>
        public Vector2 GetNormal(Vector2 position, bool isLocalCoordinate, params int[] steps)
        {
            if (!isLocalCoordinate)
                _Transform.TransformGlobalToLocal(ref position);

            Vector2 tangent = GetTangent(GetBorderIndex(ref position), steps);
            return new Vector2(-tangent.Y, tangent.X);
        }

        internal override Vector2 GetNormal(ref Vector2 position)
        {
            Vector2 tangent = GetTangent(ref position);
            return new Vector2(-tangent.Y, tangent.X);
        }

        /// <summary>
        /// Releases all references inside of the class.
        /// </summary>
        public override void Dispose()
        {
            _Border.Clear();
            _Border = null;
            _Pixels = null;
            _SourceRect = null;
            _Texture = null;

            base.Dispose();
        }

        /// <summary>
        /// Gets the enumeration type which is mapped to this kind of object
        /// </summary>
        /// <returns>the geometry typee of this object</returns>
        public override GeometryType GetGeometryType()
        {
            return GeometryType.SpriteShape;
        }

        public override LineStrip ToLineStrip()
        {
            ComputeBorder();
            return base.ToLineStrip();
        }
    }
}
